package services;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.List;

import model.Int;
import model.Nat;
import model.Rat;

/**
 * @author Henrik Wall
 * @author Rudolf Schmidt
 * @author Eugen Nemirowski
 */
public class Directory {
	/**
	 * Provides math methods.
	 */
	public static class MathService {
		/**
		 * Returns the absolute value of an {@link Int}.
		 * 
		 * @param x
		 *            the int
		 * @return the absolute value of an {@link Int}.
		 */
		public Nat abs(final Int x) {
			final Integer valueX = x.getValue();
			final int abs = Math.abs(valueX);
			return new Nat(abs);
		}
		/**
		 * Rounds a {@link Rat} to the closest {@link Int}.
		 * 
		 * @param x
		 *            the rational number to be rounded
		 * @return the closest integer
		 */
		public Int round(final Rat x) {
			final Number value = x.getValue();
			final double doubleValue = value.doubleValue();
			final long rounded = Math.round(doubleValue);
			final int casted = (int) rounded;
			return new Int(casted);
		}
		/**
		 * Rounds a {@link Rat} to a specified number of decimal places.
		 * 
		 * @param x
		 *            the rational number to be rounded
		 * @param n
		 *            the number of decimal places
		 * @return the rounded rational number
		 */
		public Rat round(final Rat x, final Nat n) {
			final Number value = x.getValue();
			final Integer scale = n.getValue();
			final double doubleValue = value.doubleValue();
			final BigDecimal bigDecimalValue = BigDecimal.valueOf(doubleValue);
			final BigDecimal scaledValue = bigDecimalValue.setScale(scale);
			final double scaledDoubleValue = scaledValue.doubleValue();
			return new Rat(scaledDoubleValue);
		}
		/**
		 * Raises a natural number to a specified power.
		 * 
		 * @param a
		 *            the base
		 * @param b
		 *            the exponent
		 * @return the value {@code a}<sup>{@code b}</sup>
		 */
		public Rat power(final Nat a, final Int b) {
			final Integer base = a.getValue();
			final Integer exponent = b.getValue();
			final int intBase = base.intValue();
			final int intExponent = exponent.intValue();
			final double powered = Math.pow(intBase, intExponent);
			return new Rat(powered);
		}
	}

	/**
	 * Searches for a service matching to the given arguments. Invokes it, if
	 * found. Otherwise throws a {@link NoServiceFoundException}.
	 * 
	 * @param actualParameters
	 *            the parameters the service should be able to process
	 * @param operationName
	 *            the name of the service operation in the syntax
	 *            {@code Directory$Service.operation}
	 * @param returnType
	 *            the expected class of the return value
	 * @return the value returned by the found service operation
	 * @throws NoServiceFoundException
	 *             if no matching service was found
	 */
	public <T> T execute(final List<?> actualParameters, final String operationName, final Class<T> returnType) {
		// the requested operation lies within an inner class, so iterate
		// through them
		for (final Class<?> innerClass : Directory.class.getDeclaredClasses()) {
			final String innerClassName = Directory.class.getSimpleName() + "$" + innerClass.getSimpleName();
			// iterate through the methods to find a match
			m: for (final Method method : innerClass.getDeclaredMethods()) {
				// we found a matching method if the following four statements
				// are true
				// (1) the method names are equal
				final boolean methodNamesMatch = operationName.equals(innerClassName + "." + method.getName());
				// (2) the requested return type is a subtype of or equal to the
				// return type of the found method
				final boolean returnTypesMatch = returnType.isAssignableFrom(method.getReturnType());
				// (3) the numbers of formal and actual parameters are the same
				final Class<?>[] formalParameterTypes = method.getParameterTypes();
				final boolean parameterCountsMatch = formalParameterTypes.length == actualParameters.size();
				if (methodNamesMatch && returnTypesMatch && parameterCountsMatch) {
					// (4) the type of every actual parameter is a subtype of or
					// equal to the formal parameter
					for (int i = 0; i < formalParameterTypes.length; i++) {
						final Class<?> formalParameterType = formalParameterTypes[i];
						final Object actualParameter = actualParameters.get(i);
						if (!formalParameterType.isInstance(actualParameter)) {
							// at least one parameter does not match, so
							// continue the search with the next method
							continue m;
						}
					}
					// we finally found a matching method, so invoke it and
					// return the result
					try {
						@SuppressWarnings("unchecked")
						final T result = (T) method.invoke(new MathService(), actualParameters.toArray());
						return result;
					} catch (IllegalAccessException | InvocationTargetException e) {
						// a problem occurred; raise it
						throw new NoServiceFoundException(actualParameters, operationName, returnType, e);
					}
				}
			}
		}
		// if we reach this point, no matching method was found
		throw new NoServiceFoundException(actualParameters, operationName, returnType);
	}
}
